﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using ModelViewerAux;

namespace ModelViewer
{
    /// <summary>
    /// This control displays the timeline at the bottom of the screen
    /// </summary>
    public partial class TimeLine : UserControl, TreeViewer.IObserver
    {
        #region Constants and Parameters

        /// <summary>
        /// The normal color of the timeline display
        /// </summary>
        private Color timelineColor = Color.White;

        /// <summary>
        /// The color of the timeline display when disabled.
        /// </summary>
        private Color timelineDisabledColor = Color.FromArgb(150, 150, 150);

        /// <summary>
        /// Left and right margins
        /// </summary>
        private const int MarginLR = 10;

        /// <summary>
        /// Bottom margin
        /// </summary>
        private const int MarginB = 10;

        private const int CurTimeX = 110;

        /// <summary>
        /// Number of pixels above the line to draw the seconds
        /// </summary>
        private const int SecondsAboveLine = 17;

        /// <summary>
        /// Length of a long tick mark
        /// </summary>
        private const int LongTickLen = 15;

        /// <summary>
        /// Length of a short tick mark
        /// </summary>
        private const int ShortTickLen = 5;

        /// <summary>
        /// Minimum spacing allowed between short ticks
        /// </summary>
        private const int MinTickSpacing = 4;

        /// <summary>
        /// How high the pointer is above the line
        /// </summary>
        const int PointerOffset = 5;

        /// <summary>
        /// The options for number of ticks per second on the timeline
        /// </summary>
        private int[] TickOptions = { 30, 15, 10, 5, 2, 1 };

        /// <summary>
        /// This variable is set true when we are dragging the pointer on the timeline
        /// </summary>
        private bool draggingPointer = false;

        /// <summary>
        /// A stopwatch we use for timing during play
        /// </summary>
        private System.Diagnostics.Stopwatch stopWatch = new System.Diagnostics.Stopwatch();

        #endregion

        #region Fields

        /// <summary>
        /// The tree viewer control that this class is associated with
        /// </summary>
        private TreeViewer treeViewer = null;

        /// <summary>
        /// The current clip duration. I retain it from the clip player
        /// so we can leave the duration after leaving a clip
        /// </summary>
        private float duration = 10.0f;

        /// <summary>
        /// Any current clip player
        /// </summary>
        private ClipPlayer currentPlayer = null;
        
        #endregion

        #region Properties

        /// <summary>
        /// The associated tree viewer
        /// </summary>
        [Browsable(false)]
        public TreeViewer TreeViewer
        {
            get { return treeViewer; }
            set { treeViewer = value; if (treeViewer != null) treeViewer.AddObserver(this); UpdateTimeline(); }
        }

        /// <summary>
        /// Timeline duration in seconds
        /// </summary>
        [Browsable(true)]
        public float Duration
        {
            get { return duration; }
            set { duration = value; }
        }



        #endregion

        #region Construction

        /// <summary>
        /// Constructor. Initialize the component and update the timeline
        /// </summary>
        public TimeLine()
        {
            InitializeComponent();

            UpdateTimeline();
        }

        #endregion

        #region Updates

        /// <summary>
        /// Observer pattern update call. Forces the timeline to be updated
        /// </summary>
        public void UpdateObserver()
        {
            UpdateTimeline();
        }

        /// <summary>
        /// Update the status of the timeline
        /// </summary>
        private void UpdateTimeline()
        {
            // This is really only at design time that this happens
            if (treeViewer == null)
                return;

            // Is there a current clip player?
            currentPlayer = null;
            if (treeViewer.SelectedClipPlayer != null)
            {
                currentPlayer = treeViewer.SelectedClipPlayer;
                duration = currentPlayer.Duration; 
            }
            else if (treeViewer.SelectedModel != null)
            {
                // If no selected clip, we use the clip from the model if it 
                // only has one clip.
                ViewerModel model = treeViewer.SelectedModel;
                if (model.ClipPlayers.Count > 0)
                {
                    currentPlayer = model.ClipPlayers[0];
                }
            }

            // Is there an active clip?
            if (currentPlayer != null)
            {
                Enabled = true;

                // I store the duration so we can display it after
                // the current player becomes null, so the duration does
                // not jump to some default value.
                duration = currentPlayer.Duration;
            }
            else
            {
                Enabled = false;
                playing = false;
            }

            labelRewind.Visible = Enabled;
            labelPlay.Visible = Enabled;
            labelEnd.Visible = Enabled;
            labelLooping.Visible = Enabled;

            labelPlay.Image = playing ? Resources.pause : Resources.play;

            Invalidate();
        }

        /// <summary>
        /// Redraw whenever we resize
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnResize(object sender, EventArgs e)
        {
            Invalidate();
        }

        #endregion

        #region Drawing

        /// <summary>
        /// Handle a paint event
        /// </summary>
        /// <param name="sender">Source of the event</param>
        /// <param name="e">Paint arguments</param>
        private void OnPaint(object sender, PaintEventArgs e)
        {
            Graphics g = e.Graphics;

            // The font to display the time with
            Font timeFont = new Font("Arial", 8);

            int lineBottom = Height - MarginB;

            Color timelineUseColor = Enabled ? timelineColor : timelineDisabledColor;

            Pen timelinePen = new Pen(timelineUseColor, 1);
            Brush timelineBrush = new SolidBrush(timelineUseColor);

            g.DrawLine(timelinePen, MarginLR, lineBottom, Width - MarginLR, lineBottom);

            // Decide the number of ticks between the seconds
            int tickCnt = 1;
            foreach (int tickOption in TickOptions)
            {
                // How are apart are the ticks?
                int space = (int)( (1.0f / (tickOption * duration) ) * (Width - MarginLR * 2) );
                if (space >= MinTickSpacing)
                {
                    tickCnt = tickOption;
                    break;
                }
            }

            // Draw the last long tick and the duration
            int x = SecToX(duration);
            string durstr = duration.ToString("F2");
            SizeF ssize2 = g.MeasureString(durstr, timeFont);
            g.DrawLine(timelinePen, x, lineBottom, x, lineBottom - LongTickLen);
            g.DrawString(durstr, timeFont, timelineBrush, new PointF(x - ssize2.Width + 1, lineBottom - SecondsAboveLine - ssize2.Height));

            // This is an amount we need to ensure we clear
            float clearX = x - ssize2.Width;

            for (int t = 0; t <= duration; t++)
            {
                // Long ticks at seconds
                x = SecToX(t);
                g.DrawLine(timelinePen, x, lineBottom, x, lineBottom - LongTickLen);

                // Labels on the seconds
                string label = t.ToString();
                SizeF ssize = g.MeasureString(label, timeFont);
                if(x + ssize.Width / 2 < clearX)
                    g.DrawString(label, timeFont, timelineBrush, new PointF(x - ssize.Width / 2, lineBottom - SecondsAboveLine - ssize.Height));

                // Short ticks between
                for (int st = 1; st < tickCnt; st++)
                {
                    float t1 = (float)st / (float)tickCnt;
                    if (t + t1 > duration)
                        break;

                    x = SecToX(t + t1);
                    g.DrawLine(timelinePen, x, lineBottom, x, lineBottom - ShortTickLen);
                }
            }

            //
            // Draw the pointer
            //
            if (Enabled && currentPlayer != null)
            {
                Image pointer = Resources.pointer;
                x = SecToX(currentPlayer.Position);
                g.DrawImage(pointer, new Point(x - pointer.Width / 2, lineBottom - PointerOffset - pointer.Height));

                string posstr = currentPlayer.Position.ToString("F2");
                g.DrawString(posstr, timeFont, timelineBrush, new PointF(CurTimeX, 0));
            }

        }

        #endregion

        #region Conversion between X positions and times

        /// <summary>
        /// Convert a time in seconds to an X position on the timeline
        /// </summary>
        /// <param name="sec"></param>
        /// <returns></returns>
        private int SecToX(float sec)
        {
            float t = duration > 0 ? sec / duration : 0;
            return (int)(MarginLR + t * (Width - MarginLR * 2));
        }

        /// <summary>
        /// Convert an X position into a time in seconds
        /// </summary>
        /// <param name="x"></param>
        /// <returns></returns>
        private float XtoSec(int x)
        {
            return ((float)(x - MarginLR) / (float)(Width - MarginLR * 2)) * duration;
        }

        #endregion

        #region Mouse Support

        /// <summary>
        /// Handle a mouse click event.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnMouseDown(object sender, MouseEventArgs e)
        {
            if (!Enabled)
                return;

            int pointerWid = Resources.pointer.Width;
            int pointerHit = Resources.pointer.Height;
            int ptrBottom = Height - MarginB - PointerOffset ;

            // Did I click on the pointer?
            int x = SecToX(currentPlayer.Position);
            if (e.X >= x - pointerWid / 2 && e.X <= x + pointerWid / 2 && e.Y >= ptrBottom - pointerHit && e.Y <= ptrBottom)
            {
                draggingPointer = true;
            }
        }

        /// <summary>
        /// Handle a mouse move event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnMouseMove(object sender, MouseEventArgs e)
        {
            if (Enabled && e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                if (draggingPointer)
                {
                    float position = XtoSec(e.X);
                    if (position < 0)
                        position = 0;

                    if (position > duration)
                        position = duration;

                    currentPlayer.Position = position;

                    Invalidate();
                }
            }
            else
            {
                draggingPointer = false;
            }
        }

        #endregion

        #region Transport Controls

        private void RewindMouseDown(object sender, MouseEventArgs e)
        {
            if (!Enabled)
                return;

            labelRewind.Image = Resources.rewindp;
            currentPlayer.Position = 0;
            Invalidate();

        }

        private void RewindMouseUp(object sender, MouseEventArgs e)
        {
            labelRewind.Image = Resources.rewind;
        }

        private void RewindMouseLeave(object sender, EventArgs e)
        {
            labelRewind.Image = Resources.rewind;
        }

        private void EndMouseDown(object sender, MouseEventArgs e)
        {
            if (!Enabled)
                return;

            labelEnd.Image = Resources.endp;
            currentPlayer.Position = currentPlayer.Duration;
            Invalidate();
        }

        private void EndMouseLeave(object sender, EventArgs e)
        {
            labelEnd.Image = Resources.end;
        }

        private void EndMouseUp(object sender, MouseEventArgs e)
        {
            labelEnd.Image = Resources.end;
        }

        /// <summary>
        /// Set true when we are playing
        /// </summary>
        private bool playing = false;

        /// <summary>
        /// The last seen time so we can determine how much time
        /// has elapsed since the last timer tick
        /// </summary>
        private long lastTime = 0;

        /// <summary>
        /// Set true if we want to loop the animation playback
        /// </summary>
        private bool looping = false;

        private void PlayClicked(object sender, EventArgs e)
        {
            if (playing)
                Pause();
            else
                Play();
        }

        private void Play()
        {
            if (playing)
                return;

            playing = true;
            labelPlay.Image = Resources.pause;

            // Start playing
            stopWatch.Start();
            lastTime = stopWatch.ElapsedMilliseconds;
            timer.Start();
        }

        private void Pause()
        {
            if (!playing)
                return;

            playing = false;
            labelPlay.Image = Resources.play;

            timer.Stop();
            stopWatch.Stop();
        }


        private void OnTimerTick(object sender, EventArgs e)
        {
            // How much time has elapsed
            long newTime = stopWatch.ElapsedMilliseconds;
            float delta = (newTime - lastTime) * 0.001f;
            lastTime = newTime;

            float newPosition = currentPlayer.Position + delta;
            if (newPosition >= currentPlayer.Duration)
            {
                // At or past the end
                if (!looping)
                {
                    currentPlayer.Position = currentPlayer.Duration;
                    Pause();
                }
                else
                {
                    // If looping, we use any overlap
                    currentPlayer.Position = newPosition - currentPlayer.Duration;
                }
            }
            else
            {
                currentPlayer.Position = newPosition;
            }

            Invalidate();
        }

        private void LoopingMouseClick(object sender, EventArgs e)
        {
            looping = !looping;
            labelLooping.Image = looping ? Resources.loopingp : Resources.looping;
        }
        
        #endregion






    }
}
